using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Urho3DNet
{
    [Generator]
    public class Urho3DNetSourceGenerator : ISourceGenerator
    {
        private static readonly HashSet<char> InvalidFileNameChars = new HashSet<char>(Path.GetInvalidFileNameChars().Concat(new[]{'<', '>'}));

        public void Execute(GeneratorExecutionContext context)
        {
            var compilation = context.Compilation;

            INamedTypeSymbol? urho3dObject = compilation.GetTypeByMetadataName("Urho3DNet.Object");
            if (urho3dObject == null)
            {
                return;
            }

            INamedTypeSymbol? derivedFromAttribute = compilation.GetTypeByMetadataName("Urho3DNet.DerivedFromAttribute");
            INamedTypeSymbol? preserveAttribute = compilation.GetTypeByMetadataName("Urho3DNet.PreserveAttribute");
            INamedTypeSymbol? rmlUIEventAttribute = compilation.GetTypeByMetadataName("Urho3DNet.RmlUIEventAttribute");
            INamedTypeSymbol? rmlUIPropertyAttribute = compilation.GetTypeByMetadataName("Urho3DNet.RmlUIPropertyAttribute");
            INamedTypeSymbol? variantListType = compilation.GetTypeByMetadataName("Urho3DNet.VariantList");

            var visitedClasses = new HashSet<string>();

            foreach (var syntaxTree in compilation.SyntaxTrees)
            {
                // Get the root of the syntax tree
                var root = syntaxTree.GetRoot();

                // Find all class declarations
                var classDeclarations = root.DescendantNodes().OfType<ClassDeclarationSyntax>();

                foreach (var classDeclaration in classDeclarations)
                {
                    var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
                    var typeSymbolInfo = semanticModel.GetDeclaredSymbol(classDeclaration) as ITypeSymbol;

                    if (typeSymbolInfo == null)
                        continue;

                    var fullClassName = typeSymbolInfo.ToString();

                    if (!visitedClasses.Add(fullClassName))
                        continue;

                    var typeHierarchy = new List<ITypeSymbol>();

                    if (!CollectHierarchyUpTo(typeSymbolInfo, urho3dObject, typeHierarchy))
                        continue;

                    if (typeHierarchy.Count > 1)
                    {
                        typeHierarchy.Reverse();

                        var sourceBuilder = new StringBuilder();

                        var className = GetClassNameWithoutNamespace(typeSymbolInfo);

                        sourceBuilder.AppendLine("// <auto-generated/>");
                        sourceBuilder.AppendLine("using Urho3DNet;");
                        if (typeSymbolInfo.ContainingNamespace != null)
                        {
                            sourceBuilder.AppendLine($"namespace {typeSymbolInfo.ContainingNamespace} {{");
                        }

                        var nestedInClasses = new List<INamedTypeSymbol>();
                        var nestedIn = typeSymbolInfo.ContainingType;
                        while (nestedIn != null)
                        {
                            nestedInClasses.Add(nestedIn);
                            nestedIn = nestedIn.ContainingType;
                        }

                        nestedInClasses.Reverse();

                        for (var index = 0; index < nestedInClasses.Count; index++)
                        {
                            var namedTypeSymbol = nestedInClasses[index];
                            sourceBuilder.AppendLine($"partial class {GetClassNameWithoutNamespace(namedTypeSymbol)} {{");
                        }

                        var newKeyword = (typeHierarchy.Count > 2) ? "new " : "";

                        var baseType = typeHierarchy[1];

                        typeHierarchy.RemoveAt(typeHierarchy.Count-1);

                        var hasClassName = HasStaticField(typeSymbolInfo, "ClassName");

                        if (!typeSymbolInfo.GetAttributes().Any(attr=> SymbolEqualityComparer.Default.Equals(attr.AttributeClass, preserveAttribute)))
                        {
                            sourceBuilder.AppendLine("[global::Urho3DNet.Preserve(AllMembers=true)]");
                        }
                        sourceBuilder.AppendLine($"partial class {className} {{");

                        bool hasGetTypeName = false;
                        bool hasGetTypeHash = false;
                        bool hasIsInstanceOf = false;
                        bool hasGetTypeNameStatic = false;
                        bool hasGetTypeStatic = false;
                        foreach (var methodSymbol in typeSymbolInfo.GetMembers().OfType<IMethodSymbol>())
                        {
                            if (methodSymbol.IsStatic)
                            {
                                switch (methodSymbol.Name)
                                {
                                    case "GetTypeNameStatic": hasGetTypeNameStatic = true; break;
                                    case "GetTypeStatic": hasGetTypeStatic = true; break;
                                }
                            }
                            else
                            {
                                switch (methodSymbol.Name)
                                {
                                    case "GetTypeName": hasGetTypeName = true; break;
                                    case "GetTypeHash": hasGetTypeHash = true; break;
                                    case "IsInstanceOf": hasIsInstanceOf = true; break;
                                }
                            }
                        }

                        if (!hasClassName)
                        {
                            if (hasGetTypeNameStatic)
                                sourceBuilder.AppendLine($"    public static new readonly string ClassName = {fullClassName}.GetTypeNameStatic();");
                            else
                                sourceBuilder.AppendLine($"    public static new readonly string ClassName = typeof({fullClassName}).GetFormattedTypeName();");
                        }

                        sourceBuilder.AppendLine($"    public static {newKeyword}readonly string BaseClassName = {baseType}.ClassName;");

                        if (hasGetTypeHash)
                            sourceBuilder.AppendLine($"    public static new readonly StringHash TypeId = {fullClassName}.GetTypeStatic();");
                        else
                            sourceBuilder.AppendLine($"    public static new readonly StringHash TypeId = new StringHash(ClassName);");

                        sourceBuilder.Append($"    public static new readonly StringHash[] TypeHierarchy = new []{{");

                        var typesAndInterfaces = typeHierarchy.Concat(CollectInterfaces(typeSymbolInfo, derivedFromAttribute)).Distinct(SymbolEqualityComparer.Default);
                        sourceBuilder.Append(string.Join(", ", typesAndInterfaces.Select(_ => $"global::Urho3DNet.ObjectReflection<{_}>.TypeId")));
                        sourceBuilder.AppendLine("};");


                        if (!hasGetTypeName)
                            sourceBuilder.AppendLine("    public override string GetTypeName() { return GetTypeNameStatic(); }");

                        if (!hasGetTypeNameStatic)
                            sourceBuilder.AppendLine("    public new static string GetTypeNameStatic() { return ClassName; }");

                        if (!hasGetTypeHash)
                            sourceBuilder.AppendLine("    public override StringHash GetTypeHash() { return GetTypeStatic(); }");

                        if (!hasGetTypeStatic)
                            sourceBuilder.AppendLine("    public new static StringHash GetTypeStatic() { return TypeId; }");

                        if (!hasIsInstanceOf)
                        {
                            sourceBuilder.AppendLine("    public override bool IsInstanceOf(StringHash typeId) {");
                            sourceBuilder.AppendLine(
                                "        for (int i=0; i<TypeHierarchy.Length; ++i) if (TypeHierarchy[i] == typeId) return true;");
                            sourceBuilder.AppendLine("        return false;");
                            sourceBuilder.AppendLine("    }");
                        }

                        var rmlProperties = new List<string>();

                        foreach (var field in classDeclaration.Members.OfType<FieldDeclarationSyntax>())
                        {
                            var variableDeclarator = field.Declaration.Variables.FirstOrDefault();
                            if (variableDeclarator == null)
                                continue;

                            var fieldSymbol = semanticModel.GetDeclaredSymbol(variableDeclarator) as IFieldSymbol;
                            if (fieldSymbol == null)
                                continue;

                            if (!fieldSymbol.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, rmlUIPropertyAttribute)))
                                continue;

                            {
                                var propertyName = GetPropertyName(fieldSymbol.Name);
                                rmlProperties.Add(propertyName);
                                sourceBuilder.AppendLine($"public {fieldSymbol.Type.ToDisplayString()} {propertyName} {{");
                                sourceBuilder.AppendLine($"get {{ return this.{fieldSymbol.Name}; }}");
                                sourceBuilder.AppendLine($"set {{ if(this.{fieldSymbol.Name} != value) {{ this.{fieldSymbol.Name} = value; DirtyVariable(\"{ propertyName}\"); }} }}");
                                sourceBuilder.AppendLine("}");
                            }
                        }


                        var rmlEvents = new List<RmlUIEventInfo>();

                        foreach (var method in classDeclaration.Members.OfType<MethodDeclarationSyntax>())
                        {
                            var methodSymbol = semanticModel.GetDeclaredSymbol(method) as IMethodSymbol;
                            if (methodSymbol == null)
                                continue;

                            if (!methodSymbol.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, rmlUIEventAttribute)))
                                continue;

                            rmlEvents.Add(new RmlUIEventInfo {
                                EventName = methodSymbol.Name,
                                Parameters = methodSymbol.Parameters,
                                NeedsAdapter = methodSymbol.Parameters.Length != 1 || !SymbolEqualityComparer.Default.Equals(methodSymbol.Parameters[0].Type, variantListType)
                            });
                        }

                        if (rmlProperties.Count > 0 || rmlEvents.Count > 0)
                        {
                            sourceBuilder.AppendLine("protected override void OnDataModelInitialized() {");
                            foreach (var p in rmlProperties)
                            {
                                sourceBuilder.AppendLine($"BindDataModelProperty(\"{p}\", value => value.Set({p}), value => {{ {p} = value; }});");
                            }
                            foreach (var p in rmlEvents)
                            {
                                var handlerName = p.NeedsAdapter ? $"Handle{p.EventName}Event" : p.EventName;
                                sourceBuilder.AppendLine($"BindDataModelEvent(\"{p.EventName}\", {handlerName});");
                            }
                            sourceBuilder.AppendLine("InitializeDataModel();");
                            sourceBuilder.AppendLine("}");
                            sourceBuilder.AppendLine("partial void InitializeDataModel();");
                        }

                        foreach (var ev in rmlEvents.Where(_ => _.NeedsAdapter))
                        {
                            sourceBuilder.AppendLine($"private void Handle{ev.EventName}Event(Urho3DNet.VariantList args) {{");
                            sourceBuilder.Append($"  {ev.EventName}(");
                            for (int index = 0; index < ev.Parameters.Length; index++)
                            {
                                IParameterSymbol? parameter = ev.Parameters[index];
                                if (index != 0)
                                    sourceBuilder.Append(", ");

                                sourceBuilder.Append($"(args.Count>{index})?({parameter.Type.ToDisplayString()})args[{index}]:default({parameter.Type.ToDisplayString()})");
                            }
                            sourceBuilder.AppendLine(");");
                            sourceBuilder.AppendLine("}");
                        }

                        sourceBuilder.AppendLine("}");

                       

                        foreach (var namedTypeSymbol in nestedInClasses)
                        {
                            sourceBuilder.AppendLine("}");
                        }

                        if (typeSymbolInfo.ContainingNamespace != null)
                        {
                            sourceBuilder.AppendLine("}");
                        }

                        string sanitizedFileName = SanitizeFileName(fullClassName);
                        context.AddSource($"{sanitizedFileName}.g.cs", sourceBuilder.ToString());
                    }
                }
            }
        }

        private string GetPropertyName(string name)
        {
            var pname = name.Trim('_');
            if (pname.Length > 0)
            {
                pname = pname.Substring(0, 1).ToUpper() + pname.Substring(1);
            }
            else
            {
                pname = name;
            }
            if (pname == name)
                pname = pname + "Property";
            return pname;
        }

        private static string SanitizeFileName(string fullClassName)
        {
            var sb = new StringBuilder(fullClassName.Length);
            foreach (var c in fullClassName)
            {
                if (InvalidFileNameChars.Contains(c))
                    sb.Append('_');
                else
                    sb.Append(c);
            }

            return sb.ToString();
        }

        private bool HasStaticField(ITypeSymbol typeSymbolInfo, string classname)
        {
            foreach (var fieldSymbol in typeSymbolInfo.GetMembers().OfType<IFieldSymbol>())
            {
                if (fieldSymbol.IsStatic && fieldSymbol.Name == "ClassName")
                {
                    return true;
                }
            }

            return false;
        }

        private string GetClassNameWithoutNamespace(ITypeSymbol typeSymbol)
        {
            var fullClassName = typeSymbol.ToString();
            return fullClassName.Substring(fullClassName.LastIndexOf('.') + 1);
        }

        private bool CollectHierarchyUpTo(ITypeSymbol? namedType, INamedTypeSymbol baseType, List<ITypeSymbol> typeHierarchy)
        {
            if (namedType == null)
                return false;

            if (SymbolEqualityComparer.Default.Equals(baseType, namedType))
            {
                typeHierarchy.Add(namedType);
                return true;
            }

            if (CollectHierarchyUpTo(namedType.BaseType, baseType, typeHierarchy))
            {
                typeHierarchy.Add(namedType);
                return true;
            }

            return  false;
        }

        private IEnumerable<ITypeSymbol> CollectInterfaces(ITypeSymbol? namedType, INamedTypeSymbol? interfaceAttribute)
        {
            if (namedType == null)
                yield break;

            if (interfaceAttribute == null)
                yield break;

            foreach (var implementedInterface in namedType.AllInterfaces)
            {
                foreach (var attr in implementedInterface.GetAttributes())
                {
                    if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, interfaceAttribute))
                    {
                        yield return implementedInterface;
                        break;
                    }
                }
            }
        }

        public static bool TryGetParentSyntax<T>(SyntaxNode? syntaxNode, out T? result) where T : SyntaxNode
        {
            result = null;
            if (syntaxNode == null)
                return false;

            syntaxNode = syntaxNode.Parent;
            if (syntaxNode == null)
                return false;

            if (syntaxNode is T tResult)
            {
                result = tResult;
                return true;
            }

            return TryGetParentSyntax(syntaxNode, out result);
        }


        public void Initialize(GeneratorInitializationContext context)
        {
            // No initialization required for this one
        }

        struct RmlUIEventInfo
        {
            public string EventName { get; set; }

            public bool NeedsAdapter { get; set; }

            public ImmutableArray<IParameterSymbol> Parameters { get; internal set; }
        }

    }

}
